Skip to content

fix: preserve human steering during Slack degradation#95

Merged
khaliqgant merged 2 commits into
mainfrom
fix/slack-freshness-webhook-healthy
Jul 17, 2026
Merged

fix: preserve human steering during Slack degradation#95
khaliqgant merged 2 commits into
mainfrom
fix/slack-freshness-webhook-healthy

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jul 17, 2026

Copy link
Copy Markdown
Member

Fixes #94.

Summary

  • Preserve model-facing [factory-needs-input] instructions for normal implementers, reviewers, and issue-driven babysitters whether or not a Slack dispatch thread exists.
  • Route clarification questions through healthy Slack when available, fall back to the source GitHub issue when Slack is stale, absent, or a confirmed write fails, and surface an operator-visible error when no route can write.
  • Normalize camel-case and snake-case webhookHealthy as an independent signal across split wrapper/nested response shapes while preserving the nested sync watermark; any explicit unhealthy signal wins. Healthy webhook delivery and actual observed event receipt may bypass only soft sync degradation; explicit error / failed status and confirmed write failures remain hard failures.
  • Keep clarification delivery, replies, parking, resume, and wake ownership durable across restart and multi-owner takeover while preserving the lifecycle and babysitter behavior from Route owned PR events to babysitter sessions #92 and Fix durable relay dispatch lifecycle #93.
  • Make GitHub fallback recoverably idempotent with deterministic markers, provider-authoritative paginated reconciliation, fail-closed tri-state handling of source/list/read/provider uncertainty, ambiguous-write watch retention, and renewed/fenced delivery leases.

Existing PR #95 containment

The original five-file webhookHealthy patch at f424c28 is fully and semantically contained in this signed candidate. The existing PR branch was updated with an explicit force-with-lease from that exact head to the audited commit; no content change beyond the signed V4 patch was required.

Validation

  • Focused changed-path suite: 413/413 passed.
  • Full suite: 39 files, 823/823 passed.
  • Former AR-54 owner-release race test passed in isolation.
  • tsc -p tsconfig.build.json --noEmit passed.
  • npm run build passed.
  • npm pack --dry-run --json --ignore-scripts passed with 266 files.
  • git diff --check passed.
  • Regression coverage includes healthy webhook arrival with falsely stale sync state; hard sync and confirmed write failures; Slack thread presence and absence; unavailable writeback; GitHub fallback and reporter authorization; accepted-but-ambiguous GitHub writes; reconciliation list/read/source failures; lease expiry and competing owners; exactly one question, one durable posted timestamp, one wake, and no lost or duplicate injection.

Independent audit

  • Commit: 475605ded5985a8efede26fa6efc2f441d3fb6fa
  • Baseline: 321c623be13beeeb3ed94e887d1df4a499e81ad3
  • Exact binary diff: 1,695 lines across 14 tracked files.
  • SHA-256: 57f3e5663abda618919db46b2f039199c08802419d9d3c0401da2e4081f9bcf8
  • Fresh independent read-only Codex audit: signed off with no blocking findings; the post-audit commit delta was reverified byte-identical to the frozen patch.

This PR is intentionally not merged by the implementation agent.

@cursor

cursor Bot commented Jul 17, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@miyaontherelay, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 59bb1995-6241-49ce-8827-633d89f3fdcc

📥 Commits

Reviewing files that changed from the base of the PR and between 96ada2f and 475605d.

📒 Files selected for processing (14)
  • src/dispatch/templates.test.ts
  • src/dispatch/templates.ts
  • src/mount/relayfile-cloud-mount-client.test.ts
  • src/mount/relayfile-cloud-mount-client.ts
  • src/orchestrator/factory.test.ts
  • src/orchestrator/factory.ts
  • src/ports/mount.ts
  • src/ports/state.ts
  • src/ports/writeback.ts
  • src/state/file-state-store.test.ts
  • src/state/file-state-store.ts
  • src/state/in-memory-state-store.ts
  • src/writeback/github.ts
  • src/writeback/writeback.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/slack-freshness-webhook-healthy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a webhookHealthy field to the ProviderSyncStatus interface and parses it from the sync status payload (supporting both camelCase and snake_case). This field is used to prevent soft degradation of Slack sync (e.g., when the sync watermark is stale but webhooks are actually healthy), ensuring that Slack writeback is not unnecessarily skipped. Comprehensive unit tests have been added to verify this behavior. I have no feedback to provide as there are no review comments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f424c28f78

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/mount/relayfile-cloud-mount-client.ts Outdated
@miyaontherelay
miyaontherelay force-pushed the fix/slack-freshness-webhook-healthy branch from f424c28 to 7bc49c5 Compare July 17, 2026 11:14
@miyaontherelay miyaontherelay changed the title fix(slack): let healthy webhook delivery veto a stale sync watermark fix: preserve human steering during Slack degradation Jul 17, 2026

@khaliqgant khaliqgant left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up on branch ownership: we collided — I had pushed a narrower fix here (f424c28) and this branch was force-pushed over it. No complaint, your version is better and I'm not pushing over it. Two findings from reviewing it against my copy, one of them a proven bug.

Your veto is better than mine — keep it

I put the veto inside slackSyncStatusResult; you put it in #slackFreshness() with slackGateBypassedByWebhookHealth + an info log. Yours is observable where mine was silent, and the soft-only/hard-still-degrades semantics match. Yours also does the thing I could only suggest in #94: decoupling the ask instructions from thread existence and routing Slack → GitHub → operator-visible error, so a degraded Slack no longer silently strips the agent's ability to ask. That's the deeper fix.

Proven bug: the hasProviderSyncFreshness change drops real freshness

Adding webhookHealthy to hasProviderSyncFreshness changes source selection. A wrapper carrying only webhook health now qualifies as the freshness-bearing candidate and wins candidates.find(hasProviderSyncFreshness), so the actual watermark on a nested connection is discarded.

Repro against this branch:

fake.getSyncStatus = async () => ({
  webhookHealthy: false,                                            // wrapper: health only, no freshness
  connections: [{ provider: 'slack', lastEventAt: '2026-06-06T12:05:00.000Z' }],  // real, stale watermark
})
await mount.getSyncStatus('slack')
// RESULT: {"provider":"slack","webhookHealthy":false}   <-- lastEventAt GONE

Consequence: a genuinely dead Slack (webhookHealthy: false + stale watermark) normalizes to no lastEventAt, no lagSeconds, no statusslackSyncStatusResult returns { known: false, degraded: false }not degraded. Factory then keeps writeback pointed at a Slack nobody reads: the question posts, no reply ever arrives, and the agent waits. That's the inverse of the failure this PR fixes, and it's the case the gate exists for.

Also still live: the Codex P2 (source-only read)

webhookHealthy: booleanField(source, ...) at relayfile-cloud-mount-client.ts:504 still reads only from source. The hasProviderSyncFreshness change masks Codex's specific example (the wrapper now wins), but by dropping the watermark rather than reading both — so the flag and the freshness can still disagree about which object they came from.

Suggested fix — one change addresses both

Revert the hasProviderSyncFreshness addition (let freshness selection stay freshness-based), and read the flag across candidates instead:

// `source` is picked for *freshness*, so webhook health can sit on a different candidate.
// An explicit `false` from any candidate wins: a wrong `true` keeps writeback pointed at a
// Slack nobody reads, which is the worse failure.
const webhookHealthySignals = [source, ...candidates]
  .map((c) => booleanField(c, 'webhookHealthy') ?? booleanField(c, 'webhook_healthy'))
  .filter((v): v is boolean => v !== undefined)
// ...
webhookHealthy: webhookHealthySignals.includes(false) ? false : webhookHealthySignals[0],

This keeps the stale watermark and sees the wrapper's flag, so the two can't disagree. Verified RED-before-green on my copy for both Codex's shape and the drop-freshness shape above.

Happy to push these two onto this branch if you'd rather not context-switch — say the word and I'll rebase onto your head rather than over it.

Field context, if useful

This is what a live Slack reports today (AgentWorkforce/cloud#2666): lastEventAt only advances on sync events and the schedule is unregistered, so it reads 6wk stale while webhooks deliver in ~8s (verified with a live post). webhookHealthy: true was correct throughout — hence this PR. Reproduced end-to-end on published 0.1.19 before the fix: relayed#48 (deliberately unanswerable from the repo) → agent never told it could ask → 30min stall, zero commits, no PR (timed out waiting for spawned agents to exit). Details in #94.

@miyaontherelay

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@miyaontherelay

Copy link
Copy Markdown
Contributor

@khaliqgant Addressed the proven split-response bug from review 4722147263 in 475605d. Freshness source selection again considers only timestamps, watermarks, and lag; webhook health is aggregated independently across applicable wrapper/nested candidates, with any explicit false winning. The new regressions prove wrapper true + nested stale freshness, wrapper false + nested stale freshness, and wrapper true conflicting with nested snake-case false while retaining the watermark. Focused 413/413 and full 823/823 passed; fresh independent audit signed off the exact full base diff SHA-256 57f3e5663abda618919db46b2f039199c08802419d9d3c0401da2e4081f9bcf8 with no blockers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slack writeback gate silently strips human-steering: a stale sync signal removes the agent's ability to ask, and the bypass reads the same broken source

2 participants